package main

import (
	"bufio"
	"context"
	crand "crypto/rand"
	"flag"
	"fmt"
	circuit "github.com/libp2p/go-libp2p-circuit"
	"github.com/libp2p/go-libp2p-core/host"
	swarm "github.com/libp2p/go-libp2p-swarm"
	"io"
	"log"
	mrand "math/rand"
	"os"
	"time"

	golog "github.com/ipfs/go-log"
	"github.com/libp2p/go-libp2p"
	"github.com/libp2p/go-libp2p-core/crypto"
	"github.com/libp2p/go-libp2p-core/network"
	"github.com/libp2p/go-libp2p-core/peer"
	ma "github.com/multiformats/go-multiaddr"
	gologging "github.com/whyrusleeping/go-logging"
)

// generateIdentity generates a RSA(2048) private key.
// If the seed is zero, use real cryptographic randomness. Otherwise, use a
// deterministic randomness source to make generated keys stay the same
// across multiple runs
func generateIdentity(seed int64) (identity crypto.PrivKey, err error) {
	var r io.Reader
	if seed == 0 {
		r = crand.Reader
	} else {
		r = mrand.New(mrand.NewSource(seed))
	}
	identity, _, err = crypto.GenerateKeyPairWithReader(crypto.RSA, 2048, r)
	return identity, err
}

func makeHost(identity crypto.PrivKey, insecure bool) (host.Host, error) {
	opts := []libp2p.Option{
		libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0"),
		libp2p.Identity(identity),
		libp2p.EnableRelay(circuit.OptDiscovery),
	}
	if insecure {
		opts = append(opts, libp2p.NoSecurity)
		log.Println("[WARN] Insecure relay")
	}
	return libp2p.New(context.Background(), opts...)
}

func connectRelay(h host.Host, relay string) error {
	relayAddr, err := ma.NewMultiaddr(relay)
	if err != nil {
		return err
	}
	pid, err := relayAddr.ValueForProtocol(ma.P_P2P)
	if err != nil {
		return err
	}
	relayPeerID, err := peer.IDB58Decode(pid)
	if err != nil {
		return err
	}

	relayPeerAddr, _ := ma.NewMultiaddr(fmt.Sprintf("/p2p/%s", pid))
	relayAddress := relayAddr.Decapsulate(relayPeerAddr)

	peerInfo := peer.AddrInfo{
		ID:    relayPeerID,
		Addrs: []ma.Multiaddr{relayAddress},
	}
	return h.Connect(context.Background(), peerInfo)
}

func printSelf(h host.Host) {
	hostAddr, _ := ma.NewMultiaddr(fmt.Sprintf("/p2p/%s", h.ID().Pretty()))
	addr := h.Addrs()[0]
	fullAddr := addr.Encapsulate(hostAddr)
	log.Printf("I am %s\n", fullAddr)
}

func readChatMessage(s network.Stream) {
	buf := bufio.NewReader(s)
	for {
		str, err := buf.ReadString('\n')
		if err != nil {
			log.Fatal("[ERROR] read chat message error", err)
		}
		log.Println("[CHAT] ", str)
	}
}

func beginChat(s network.Stream) error {
	reader := bufio.NewReader(os.Stdin)
	var err error
	for {
		text, _ := reader.ReadString('\n')
		_, err = s.Write([]byte(text))
		if err != nil {
			break
		}
	}
	return err
}

func connectChat(h host.Host, targetPeerID string) error {
	peerID, err := peer.IDB58Decode(targetPeerID)
	if err != nil {
		return err
	}
	h.Network().(*swarm.Swarm).Backoff().Clear(peerID)

	var address = fmt.Sprintf("/p2p-circuit/ipfs/%s", peerID.Pretty())
	chatTargetAddr, err := ma.NewMultiaddr(address)
	if err != nil {
		return err
	}

	chatTargetAddrInfo := peer.AddrInfo{
		ID:    peerID,
		Addrs: []ma.Multiaddr{chatTargetAddr},
	}
	if err := h.Connect(context.Background(), chatTargetAddrInfo); err != nil {
		return err
	}

	log.Println("opening chat stream")
	// make a new stream from host B to host A
	// it should be handled on host A by the handler we set above because
	// we use the same /chat/1.0.0 protocol
	s, err := h.NewStream(context.Background(), peerID, "/chat/1.0.0")
	if err != nil {
		return err
	}
	log.Println("[INFO] chat connected!")
	go readChatMessage(s)
	return beginChat(s)
}

func main() {
	golog.SetAllLoggers(gologging.INFO)

	// Parse options from the command line
	targetPeerID := flag.String("d", "", "target peer ID")
	insecure := flag.Bool("insecure", false, "use an unencrypted connection")
	seed := flag.Int64("seed", 0, "set random seed for id generation")
	relay := flag.String("relay", "", "relay server")
	flag.Parse()
	if *relay == "" {
		log.Fatal("")
	}

	identity, err := generateIdentity(*seed)
	if err != nil {
		log.Fatal(err)
	}

	h, err := makeHost(identity, *insecure)
	if err != nil {
		log.Fatal(err)
	}

	err = connectRelay(h, *relay)
	if err != nil {
		log.Fatal(err)
	}
	printSelf(h)

	if *targetPeerID == "" {
		streamChan := make(chan network.Stream)
		h.SetStreamHandler("/chat/1.0.0", func(s network.Stream) {
			streamChan <- s
		})
		log.Println("listening for chat connections")
		chatStream := <-streamChan
		log.Println("[INFO] chat connected!")
		go readChatMessage(chatStream)
		err = beginChat(chatStream)
		log.Fatal(err)
	}
	time.Sleep(1 * time.Second)
	err = connectChat(h, *targetPeerID)
	log.Fatal(err)
}
